SPDX-FileCopyrightText: 2025 ChloƩ Coppens & Lara Pietkowicz SPDX-FileCopyrightText: 2025 AlICe laboratory https://alicelab.be
SPDX-License-Identifier: GPL-3.0-or-later
import bpy
import bmesh
import pprint
import randomdef clean():
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete(use_global=False)
    bpy.ops.outliner.orphans_purge()
clean()positions = []
partition = []
for _ in range(43):
    positions.append(random.choice([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
    note = []
    for _ in range(random.randint(1, 7)):
        note.append(random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9]))
    partition.append(note)Ellen’s script data partition = [ [1, 5, 2, 7, 3], [4, 5, 8, 1, 9], [1, 5, 2, 7], [3, 1, 2], [3, 1, 2], [9], [1, 5, 2, 5], [8, 1, 9], [8, 1, 9, 1], [6, 3, 2, 7], [8, 1, 9], [8, 1, 9, 4], [1, 5, 4, 9], [1, 4, 6, 3], [1, 5, 4, 9], [1, 4, 6, 3], [1, 5, 4, 9], [2, 5], [6, 3, 2, 5], [6, 3, 2, 5], [1, 5, 2, 5], [9], [1, 4], [1, 4], [6, 3, 2, 5], [1, 5, 4, 9], [2, 7, 3], [7, 1, 1], [6], [6], [3], [3], [2, 7, 3], [6, 3, 2, 5], [6, 3, 2, 5], [5, 5, 3], [6, 3, 2, 5], [6, 3, 2, 5], [5, 5, 3], [6, 3, 2, 5], [6, 3, 2, 5], [8, 1, 9, 1], [4, 5, 8, 1, 9] ] positions = [5, 2, 7, 4, 4, 5, 7, 5, 3, 0, 2, 3, 5, 6, 7, 6, 5, 3, 2, 2, 2, 4, 3, 3, 3, 5, 4, 4, 3, 3, 3, 3, 3, 6, 6, 4, 6, 2, 2, 6, 6, 5, 2]
Braille data
braille_values = {
    1: [0],
    2: [0, 2],
    3: [0, 1],
    4: [0, 1, 3],
    5: [0, 3],
    6: [0, 1, 2],
    7: [0, 1, 2, 3],
    8: [0, 2, 3],
    9: [1, 2],
}def coordinate_from_position(pos, n):
    y = plane_distance if n % 2 == 0 else -plane_distance
    print("In:", n, y, pos, end=" ")
    return [n, y, pos * z_step]2.2 Generate plane surfaces 2.2.1 Left side
bpy.ops.mesh.primitive_plane_add(location=(30, -36.3, 28))
bpy.context.object.name = "gege-gauche"
bpy.ops.transform.resize(value=(38, 38, 38))
bpy.ops.transform.rotate(value=1.571, orient_axis="X")
bpy.ops.object.modifier_add(type="SOLIDIFY")
bpy.context.object.modifiers["Solidify"].thickness = 0.042.2.2 Right side
bpy.ops.mesh.primitive_plane_add(location=(30, 37.8, 28))
bpy.context.object.name = "gege-droite"
bpy.ops.transform.resize(value=(38, 38, 38))
bpy.ops.transform.rotate(value=1.571, orient_axis="X")
bpy.ops.object.modifier_add(type="SOLIDIFY")
bpy.context.object.modifiers["Solidify"].thickness = 0.042.3 Generate Braille’s pattern
def braille_sequence(coord, notes):
    print("with patterns:")
    note_sequence_dot_coordinates = []
    for single_note in notes:
        braille_pattern = braille_values[single_note]
        print("braille:", braille_pattern)
        brailles_dot_coords = braille(coord, braille_pattern)
        note_sequence_dot_coordinates.append(brailles_dot_coords)
        coord[0] += braille_steps
    return note_sequence_dot_coordinates2.4 Braille’s parameters
sphere_radius = 0.35
braille_steps = 2
plane_distance = 76 / 2
braille_dot_offset = 0.5
z_step = 2.32.5 Generate Braille’s sphere according to pattern
def braille(coord, pattern):
    new_coord = coord.copy()
    dot_coordinates = []
    for dot in pattern:
        if dot == 0:
            new_coord[0] -= braille_dot_offset
            new_coord[2] += braille_dot_offsetmake a sphere and save coordinate
            print("a sphere in", new_coord)
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=sphere_radius, location=new_coord
            )
            dot_coordinates.append(tuple(new_coord))reset coordinate
            new_coord = coord.copy()
        elif dot == 1:
            new_coord[0] += braille_dot_offset
            new_coord[2] += braille_dot_offsetmake a sphere and save coordinate
            print("a sphere in", new_coord)
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=sphere_radius, location=new_coord
            )
            dot_coordinates.append(tuple(new_coord))reset coordinate
            new_coord = coord.copy()
        elif dot == 2:
            new_coord[0] += braille_dot_offset
            new_coord[2] -= braille_dot_offsetmake a sphere and save coordinate
            print("a sphere in", new_coord)
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=sphere_radius, location=new_coord
            )
            dot_coordinates.append(tuple(new_coord))reset coordinate
            new_coord = coord.copy()
        elif dot == 3:
            new_coord[0] = new_coord[0] - braille_dot_offset
            new_coord[2] = new_coord[2] - braille_dot_offsetmake a sphere and save coordinate
            print("a sphere in", new_coord)
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=sphere_radius, location=new_coord
            )
            dot_coordinates.append(tuple(new_coord))reset coordinate
            new_coord = coord.copy()
    return dot_coordinates2.6 Generate connections between notes 2.6.1 Generate dots according to the spheres
print("MAKE DOTS")
generated_dots = []
length = 0
for n, pos in enumerate(positions):
    coord = coordinate_from_position(pos * 3, n)
    coord[0] += length
    notes = partition[n]
    print("use", notes, end=" ")
    braille_sequence_dots = braille_sequence(coord, notes)
    length = len(braille_sequence_dots)
    generated_dots.append(braille_sequence_dots)
pprint.pprint(generated_dots)2.6.2 Generate connections between line and dots
print("MAKE LINES")
n = 0
print("A Note:", generated_dots[n])
print("Next Note:", generated_dots[n + 1])
print("A Note's first braille:", generated_dots[n][0])
print("Next Note's first braille:", generated_dots[n + 1][0])
print("Next Note's first braille first dot:", generated_dots[n + 1][0][0])2.7 Generate a line
def make_line(point1, point2, obj_name="custom_line"):Make a line from two points. create a list of vertex coordinates
    vert_coords = [point1, point2]create the mesh data
    mesh_data = bpy.data.meshes.new(f"{obj_name}_data")create the mesh object using the mesh data
    mesh_obj = bpy.data.objects.new(obj_name, mesh_data)add the mesh object into the scene
    bpy.context.scene.collection.objects.link(mesh_obj)create a new bmesh
    bm = bmesh.new()create and add a vertices
    for coord in vert_coords:
        bm.verts.new(coord)connect vertices into edge
    bm.verts.ensure_lookup_table()
    v1, v2 = bm.verts[0], bm.verts[1]
    bm.edges.new((v1, v2))writes the bmesh data into the mesh data
    bm.to_mesh(mesh_data)[Optional] update the mesh data (helps with redrawing the mesh in the viewport)
    mesh_data.update()clean up/free memory that was allocated for the bmesh
    bm.free()return object for later use
    return bpy.data.objects[obj_name]def get_dot_coordinates(note):
    dot_coord = []
    for braille in note:
        for dot in braille:
            dot_coord.append(dot)
    return dot_coorddef connect_brailles(note1, note2):
    side1 = get_dot_coordinates(note1)
    side2 = get_dot_coordinates(note2)
    random.shuffle(side1)
    random.shuffle(side2)
    if len(side1) > len(side2):
        reserve = side2.copy()
        for n, dot1 in enumerate(side1):
            if n < len(side2):
                make_line(dot1, side2.pop())
            else:
                make_line(dot1, random.choice(reserve))
    else:
        for dot1 in side1:
            reserve = side1.copy()
            for n, dot2 in enumerate(side2):
                if n < len(side1):
                    make_line(dot2, side1.pop())
                else:
                    make_line(dot2, random.choice(reserve))2.8 Convert lines in mesh
note_number = len(generated_dots)
for n in range(note_number):
    if n + 1 < note_number:
        connect_brailles(generated_dots[n], generated_dots[n + 1])2.9 Convert mesh in tubes
for obj in bpy.data.objects:
    if "line" in obj.name:
        obj.select_set(True)
        bpy.context.view_layer.objects.active = obj
        bpy.ops.object.convert(target="CURVE")
        bpy.context.object.data.bevel_depth = 0.2
        bpy.context.object.data.use_fill_caps = True